﻿using System.Text.RegularExpressions;
using DirectoryServicesProtocolsExtensions.MetaData;

namespace DirectoryServicesProtocolsExtensions.LdapConnection
{
    using System;
    using System.DirectoryServices.Protocols;
    using System.Linq;

    public static class Schema
    {
        private const string SyntaxRegexPattern =
                 @"(?<syntaxLabel>\bSYNTAX\b)\s+(?<syntaxValue>'?\d\.[\d.]+)";
        // example "( 2.5.4.4 NAME ( 'sn' 'surname' ) SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{64} 
        private const string NameRegexForSynonims = 
                @"(?<nameLabel>\bNAME\b)\s+(?<nameValue>\(\s+'[\w|-]+'\s+'[\w|-]+'\s\))";

       private static readonly SearchRequest RootDSESearchRequest = new SearchRequest(
            null,
            "(objectClass=*)",
            SearchScope.Base,
            new[] { "defaultNamingContext", "schemaNamingContext", "subschemaSubentry" });

        private static readonly SearchRequest SubSchemaSubEntrySearchRequest = new SearchRequest(
            null,
            "(objectClass=subschemaSubentry)",
            SearchScope.Base,
            new[] { "subschemaSubentry" });

        private static readonly SearchOptionsControl SearchOptionsControlDomainScope =
            new SearchOptionsControl(SearchOption.DomainScope);

        private static bool IsAttributeNameFoundInSchemaRecord(string rawSchemaRecord, string attributeLdapDisplayName)
        {
            var nameSynonimRegex = new Regex(NameRegexForSynonims);
            var synonimMatch = nameSynonimRegex.Match(rawSchemaRecord);


            return (rawSchemaRecord.IndexOf("NAME " + "'" + attributeLdapDisplayName + "'", StringComparison.OrdinalIgnoreCase) > 0) 
                || (synonimMatch.Success && synonimMatch.Groups["nameValue"].Value.Contains(attributeLdapDisplayName));
        }

        private static string ExtractRFC2252Syntax(string attributeSchemaDefinition)
        {
            var syntaxRegex = new Regex(SyntaxRegexPattern);
            var match = syntaxRegex.Match(attributeSchemaDefinition);
            if (!match.Success)
            {
                throw new Exception(
                    string.Format("Unable to find SYNTAX within attribute schema definition record: {0}",
                        attributeSchemaDefinition));
            }

            return match.Groups["syntaxValue"].Value.Replace("'", string.Empty);
        }

        private static bool ExtractMultiValueDefinition(string attributeSchemaDefinition)
        {
            return !attributeSchemaDefinition.Contains("SINGLE-VALUE");
        }

        // Originally implemented this method with caching capability - subsequent requests would utilize a cached
        // version of the schema. But my tests showed that this measure was not necessary, since it appears the .NET
        // caches the results of the query internally.
        public static LdapAttributeMetaMap GetAttributeMetaData(
            this LdapConnection ldapConnection,
            string attributeLdapDisplayName,
            LdapMetaMapFactory ldapMetaMapFactory)
        {
           var schemaDiscoveryRequest = new SearchRequest(
                        GetSubschemaSubentryDN(ldapConnection),
                        "(objectClass=*)",
                        SearchScope.Base,
                        new[] { "attributeTypes" });

            var searchResponse = (SearchResponse)ldapConnection.SendRequest(schemaDiscoveryRequest);

            if (searchResponse == null || searchResponse.Entries[0].Attributes["attributeTypes"] == null)
            {
               throw new Exception(string.Format("Unable to parse attributeTypes in {0}", schemaDiscoveryRequest.DistinguishedName)); 
            }

            
            var rawFullSchema = (string[])searchResponse.Entries[0].Attributes["attributeTypes"].GetValues(typeof(string));
            var rawAttributeSchemaMatches =
                    rawFullSchema.Where(a => IsAttributeNameFoundInSchemaRecord(a, attributeLdapDisplayName)).ToArray();

            if (rawAttributeSchemaMatches.Count() > 1)
            {
                throw new Exception(string.Format("Multiple Schema Records for attribute {0} were detected", attributeLdapDisplayName));
            }

            if (!rawAttributeSchemaMatches.Any())
            {
                return null;
            }

            return ldapMetaMapFactory.Create(
                        attributeLdapDisplayName,
                        ExtractRFC2252Syntax(rawAttributeSchemaMatches[0]),
                        ExtractMultiValueDefinition(rawAttributeSchemaMatches[0]));
        }

        public static string GetSubschemaSubentryDN(this LdapConnection ldapConnection)
        {
            // SubSchemaSubEntrySearchRequest.Controls.Add(SearchOptionsControlDomainScope);
            var searchResponse = (SearchResponse)ldapConnection.SendRequest(SubSchemaSubEntrySearchRequest);
            if (searchResponse != null)
            {
                return searchResponse.Entries[0].Attributes["subschemaSubentry"][0].ToString();
            }

            throw new Exception("Unable to locate subschemaSubentry attribute in RootDSE");
        }

        public static string GetDefaultNamingContextDN(this LdapConnection ldapConnection)
        {
            RootDSESearchRequest.Controls.Add(SearchOptionsControlDomainScope);
            var searchResponse = (SearchResponse)ldapConnection.SendRequest(RootDSESearchRequest);
            if (searchResponse != null)
            {
                return searchResponse.Entries[0].Attributes["defaultNamingContext"][0].ToString();
            }

            throw new Exception("Unable to locate default naming context attribute in RootDSE");
        }

        

        
    }
}
